<template>
  <div class="normal-map">
    <h1>法线贴图</h1>
    <p>
      法线贴图（Normal Map）是一种用于增强3D物体表面细节的纹理映射技术。通过在纹理中存储表面法线信息，可以在不增加模型几何复杂度的情况下，实现更丰富的光照效果和表面细节。
    </p>

    <el-card class="demo-section">
      <template #header>
        <div class="card-header">
          <h3>法线贴图示例</h3>
        </div>
      </template>
      <div class="demo-content">
        <canvas ref="canvas" width="400" height="400"></canvas>
        <div class="control-panel">
          <el-form :model="normalMapForm" label-width="120px">
            <el-form-item label="法线贴图强度">
              <el-slider v-model="normalMapForm.normalScale" :min="0" :max="5" :step="0.1" @change="updateNormalScale" />
            </el-form-item>
            <el-form-item label="深度测试">
              <el-switch v-model="normalMapForm.depthTest" @change="updateDepthTest" />
            </el-form-item>
          </el-form>
          <p>这是一个带有法线贴图的立方体，你可以：</p>
          <ul>
            <li>调整法线贴图的强度</li>
            <li>开启/关闭深度测试</li>
          </ul>
        </div>
      </div>
    </el-card>

    <el-card class="code-section">
      <template #header>
        <div class="card-header">
          <h3>示例代码</h3>
        </div>
      </template>
      <div class="code-content">
        <pre><code>
// 顶点着色器
attribute vec4 a_Position;
attribute vec2 a_TexCoord;
attribute vec3 a_Normal;
attribute vec3 a_Tangent;
varying vec2 v_TexCoord;
varying vec3 v_Normal;
varying vec3 v_Tangent;
varying vec3 v_Position;
uniform mat4 u_ModelMatrix;
uniform mat4 u_ViewMatrix;
uniform mat4 u_ProjMatrix;
uniform mat3 u_NormalMatrix;  // 添加法线矩阵

void main() {
  gl_Position = u_ProjMatrix * u_ViewMatrix * u_ModelMatrix * a_Position;
  v_TexCoord = a_TexCoord;
  v_Normal = normalize(u_NormalMatrix * a_Normal);  // 使用法线矩阵
  v_Tangent = normalize(u_NormalMatrix * a_Tangent);  // 切线也需要使用法线矩阵
  v_Position = (u_ModelMatrix * a_Position).xyz;
}

// 片元着色器
precision mediump float;
varying vec2 v_TexCoord;
varying vec3 v_Normal;
varying vec3 v_Tangent;
varying vec3 v_Position;
uniform sampler2D u_DiffuseMap;
uniform sampler2D u_NormalMap;
uniform vec3 u_LightPosition;
uniform float u_NormalScale;

void main() {
  // 1. 构建 TBN 矩阵，这是一个从切线空间到世界空间的变换矩阵
  // T: 切线空间的 X 轴在世界空间中的方向
  // B: 切线空间的 Y 轴在世界空间中的方向
  // N: 切线空间的 Z 轴在世界空间中的方向
  vec3 N = normalize(v_Normal);
  vec3 T = normalize(v_Tangent);
  vec3 B = normalize(cross(N, T));
  mat3 TBN = mat3(T, B, N);

  // 2. normalMap 是在切线空间中的一个方向向量
  // 法线贴图中的 RGB 分量分别对应切线空间的 XYZ 轴
  // 从 [0,1] 转换到 [-1,1] 范围
  vec3 normalMap = texture2D(u_NormalMap, v_TexCoord).rgb * 2.0 - 1.0;
  normalMap.xy *= u_NormalScale;
  normalMap = normalize(normalMap);
  // 默认情况下 normalMap 通常是 (0,0,1)，表示面完全朝上
  // 当表面有凹凸时，会偏离这个方向，比如 (0.5,0.5,0.7) 表示向右上倾斜

  // 3. TBN * normalMap 是一个基底变换
  // 将切线空间中的方向向量转换到世界空间
  vec3 normal = normalize(TBN * normalMap);

  // 计算光照
  vec3 lightDir = normalize(u_LightPosition - v_Position);
  float diff = max(dot(normal, lightDir), 0.0);
  vec3 diffuse = texture2D(u_DiffuseMap, v_TexCoord).rgb;
  vec3 ambient = diffuse * 0.3; // 降低环境光强度以增强对比度

  // 使用更强的对比度
  diff = pow(diff, 1.0 / max(u_NormalScale, 0.5)); // 根据法线贴图强度调整对比度

  // 添加简单的高光计算
  vec3 viewDir = normalize(-v_Position); // 视线方向
  vec3 halfwayDir = normalize(lightDir + viewDir); // 半程向量
  float spec = pow(max(dot(normal, halfwayDir), 0.0), 32.0); // 高光系数
  vec3 specular = vec3(0.3) * spec; // 高光颜色

  gl_FragColor = vec4(ambient + diffuse * diff + specular, 1.0);
}
        </code></pre>
      </div>
    </el-card>
  </div>
</template>

<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { WebGLUtils } from '../../utils/webgl'
import { mat4, mat3 } from 'gl-matrix'  // 添加 gl-matrix 导入

// WebGL相关变量
const canvas = ref<HTMLCanvasElement | null>(null)
let gl: WebGLRenderingContext | null = null
let program: WebGLProgram | null = null

// 顶点着色器
const VSHADER_SOURCE = `
attribute vec4 a_Position;
attribute vec2 a_TexCoord;
attribute vec3 a_Normal;
attribute vec3 a_Tangent;
varying vec2 v_TexCoord;
varying vec3 v_Normal;
varying vec3 v_Tangent;
varying vec3 v_Position;
uniform mat4 u_ModelMatrix;
uniform mat4 u_ViewMatrix;
uniform mat4 u_ProjMatrix;
uniform mat3 u_NormalMatrix;  // 添加法线矩阵

void main() {
  gl_Position = u_ProjMatrix * u_ViewMatrix * u_ModelMatrix * a_Position;
  v_TexCoord = a_TexCoord;
  v_Normal = normalize(u_NormalMatrix * a_Normal);  // 使用法线矩阵
  v_Tangent = normalize(u_NormalMatrix * a_Tangent);  // 切线也需要使用法线矩阵
  v_Position = (u_ModelMatrix * a_Position).xyz;
}
`

// 片元着色器
const FSHADER_SOURCE = `
precision mediump float;
varying vec2 v_TexCoord;
varying vec3 v_Normal;
varying vec3 v_Tangent;
varying vec3 v_Position;
uniform sampler2D u_DiffuseMap;
uniform sampler2D u_NormalMap;
uniform vec3 u_LightPosition;
uniform float u_NormalScale;

void main() {
  // 1. 构建 TBN 矩阵，这是一个从切线空间到世界空间的变换矩阵
  // T: 切线空间的 X 轴在世界空间中的方向
  // B: 切线空间的 Y 轴在世界空间中的方向
  // N: 切线空间的 Z 轴在世界空间中的方向
  vec3 N = normalize(v_Normal);
  vec3 T = normalize(v_Tangent);
  vec3 B = normalize(cross(N, T));
  mat3 TBN = mat3(T, B, N);

  // 2. normalMap 是在切线空间中的一个方向向量
  // 法线贴图中的 RGB 分量分别对应切线空间的 XYZ 轴
  // 从 [0,1] 转换到 [-1,1] 范围
  vec3 normalMap = texture2D(u_NormalMap, v_TexCoord).rgb * 2.0 - 1.0;
  normalMap.xy *= u_NormalScale;
  normalMap = normalize(normalMap);
  // 默认情况下 normalMap 通常是 (0,0,1)，表示面完全朝上
  // 当表面有凹凸时，会偏离这个方向，比如 (0.5,0.5,0.7) 表示向右上倾斜

  // 3. TBN * normalMap 是一个基底变换
  // 将切线空间中的方向向量转换到世界空间
  vec3 normal = normalize(TBN * normalMap);

  // 计算光照
  vec3 lightDir = normalize(u_LightPosition - v_Position);
  float diff = max(dot(normal, lightDir), 0.0);
  vec3 diffuse = texture2D(u_DiffuseMap, v_TexCoord).rgb;
  vec3 ambient = diffuse * 0.4;

  gl_FragColor = vec4(ambient + diffuse * diff, 1.0);
}
`

// 表单数据
const normalMapForm = reactive({
  normalScale: 1.0,
  depthTest: true
})

// 更新法线贴图强度
const updateNormalScale = () => {
  if (!gl || !program) return
  const u_NormalScale = gl.getUniformLocation(program, 'u_NormalScale')
  gl.uniform1f(u_NormalScale, normalMapForm.normalScale)
}

// 设置固定光源位置
const setLightPosition = () => {
  if (!gl || !program) return
  const u_LightPosition = gl.getUniformLocation(program, 'u_LightPosition')
  // 设置固定的光源位置，选择一个能够更好地展示法线贴图效果的位置
  gl.uniform3f(u_LightPosition, 2.0, 2.0, 3.0)
}

// 更新深度测试
const updateDepthTest = () => {
  if (!gl) return
  if (normalMapForm.depthTest) {
    gl.enable(gl.DEPTH_TEST)
  } else {
    gl.disable(gl.DEPTH_TEST)
  }
}

// 初始化顶点缓冲区
const initVertexBuffers = () => {
  if (!gl || !program) return

  const vertices = new Float32Array([
    // 位置、纹理坐标、法线、切线
    // 前面 (z = 0.5)
     0.5,  0.5,  0.5,  1.0, 1.0,  0.0,  0.0,  1.0,  1.0,  0.0,  0.0,
    -0.5,  0.5,  0.5,  0.0, 1.0,  0.0,  0.0,  1.0,  1.0,  0.0,  0.0,
    -0.5, -0.5,  0.5,  0.0, 0.0,  0.0,  0.0,  1.0,  1.0,  0.0,  0.0,
     0.5, -0.5,  0.5,  1.0, 0.0,  0.0,  0.0,  1.0,  1.0,  0.0,  0.0,
    // 后面 (z = -0.5)
     0.5,  0.5, -0.5,  1.0, 1.0,  0.0,  0.0, -1.0, -1.0,  0.0,  0.0,
    -0.5,  0.5, -0.5,  0.0, 1.0,  0.0,  0.0, -1.0, -1.0,  0.0,  0.0,
    -0.5, -0.5, -0.5,  0.0, 0.0,  0.0,  0.0, -1.0, -1.0,  0.0,  0.0,
     0.5, -0.5, -0.5,  1.0, 0.0,  0.0,  0.0, -1.0, -1.0,  0.0,  0.0,
    // 右面 (x = 0.5)
     0.5,  0.5,  0.5,  1.0, 1.0,  1.0,  0.0,  0.0,  0.0,  0.0, -1.0,
     0.5,  0.5, -0.5,  0.0, 1.0,  1.0,  0.0,  0.0,  0.0,  0.0, -1.0,
     0.5, -0.5, -0.5,  0.0, 0.0,  1.0,  0.0,  0.0,  0.0,  0.0, -1.0,
     0.5, -0.5,  0.5,  1.0, 0.0,  1.0,  0.0,  0.0,  0.0,  0.0, -1.0,
    // 左面 (x = -0.5)
    -0.5,  0.5,  0.5,  1.0, 1.0, -1.0,  0.0,  0.0,  0.0,  0.0,  1.0,
    -0.5,  0.5, -0.5,  0.0, 1.0, -1.0,  0.0,  0.0,  0.0,  0.0,  1.0,
    -0.5, -0.5, -0.5,  0.0, 0.0, -1.0,  0.0,  0.0,  0.0,  0.0,  1.0,
    -0.5, -0.5,  0.5,  1.0, 0.0, -1.0,  0.0,  0.0,  0.0,  0.0,  1.0,
    // 上面 (y = 0.5)
     0.5,  0.5,  0.5,  1.0, 1.0,  0.0,  1.0,  0.0,  1.0, -1.0,  0.0,
    -0.5,  0.5,  0.5,  0.0, 1.0,  0.0,  1.0,  0.0,  1.0, -1.0,  0.0,
    -0.5,  0.5, -0.5,  0.0, 0.0,  0.0,  1.0,  0.0,  1.0, -1.0,  0.0,
     0.5,  0.5, -0.5,  1.0, 0.0,  0.0,  1.0,  0.0,  1.0, -1.0,  0.0,
    // 下面 (y = -0.5)
     0.5, -0.5,  0.5,  1.0, 1.0,  0.0, -1.0,  0.0, -1.0,  1.0,  0.0,
    -0.5, -0.5,  0.5,  0.0, 1.0,  0.0, -1.0,  0.0, -1.0,  1.0,  0.0,
    -0.5, -0.5, -0.5,  0.0, 0.0,  0.0, -1.0,  0.0, -1.0,  1.0,  0.0,
     0.5, -0.5, -0.5,  1.0, 0.0,  0.0, -1.0,  0.0, -1.0,  1.0,  0.0,
  ])

  const indices = new Uint8Array([
    0, 1, 2,   0, 2, 3,    // 前面
    16, 17, 18,   16, 18, 19,    // 右面
    4, 5, 6,   4, 6, 7,    // 后面
    12, 13, 14,   12, 14, 15,    // 左面
    8, 9, 10,   8, 10, 11,    // 上面
    20, 21, 22,   20, 22, 23     // 下面
  ])

  // 创建缓冲区对象
  const vertexBuffer = gl.createBuffer()
  const indexBuffer = gl.createBuffer()
  if (!vertexBuffer || !indexBuffer) {
    console.error('Failed to create buffer object')
    return
  }

  gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
  gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW)

  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer)
  gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW)

  const FSIZE = vertices.BYTES_PER_ELEMENT
  const STRIDE = FSIZE * 11

  const a_Position = gl.getAttribLocation(program, 'a_Position')
  gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, STRIDE, 0)
  gl.enableVertexAttribArray(a_Position)

  const a_TexCoord = gl.getAttribLocation(program, 'a_TexCoord')
  gl.vertexAttribPointer(a_TexCoord, 2, gl.FLOAT, false, STRIDE, FSIZE * 3)
  gl.enableVertexAttribArray(a_TexCoord)

  const a_Normal = gl.getAttribLocation(program, 'a_Normal')
  gl.vertexAttribPointer(a_Normal, 3, gl.FLOAT, false, STRIDE, FSIZE * 5)
  gl.enableVertexAttribArray(a_Normal)

  const a_Tangent = gl.getAttribLocation(program, 'a_Tangent')
  gl.vertexAttribPointer(a_Tangent, 3, gl.FLOAT, false, STRIDE, FSIZE * 8)
  gl.enableVertexAttribArray(a_Tangent)
}

// 初始化纹理
const initTextures = () => {
  if (!gl || !program) return

  // 创建并设置漫反射贴图
  const diffuseTexture = gl.createTexture()
  const normalTexture = gl.createTexture()
  if (!diffuseTexture || !normalTexture) {
    console.error('Failed to create texture object')
    return
  }

  // 加载漫反射贴图
  const diffuseImage = new Image()
  diffuseImage.crossOrigin = 'anonymous'
  diffuseImage.onload = () => {
    if (!gl || !program) return
    gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1)
    gl.activeTexture(gl.TEXTURE0)
    gl.bindTexture(gl.TEXTURE_2D, diffuseTexture)
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, diffuseImage)
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR)
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
    gl.generateMipmap(gl.TEXTURE_2D)

    const u_DiffuseMap = gl.getUniformLocation(program, 'u_DiffuseMap')
    gl.uniform1i(u_DiffuseMap, 0)
  }
  diffuseImage.src = '/texture/raw_plank_wall_diff_2k.jpg'

  // 加载法线贴图
  const normalImage = new Image()
  normalImage.crossOrigin = 'anonymous'
  normalImage.onload = () => {
    if (!gl || !program) return
    gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1)
    gl.activeTexture(gl.TEXTURE1)
    gl.bindTexture(gl.TEXTURE_2D, normalTexture)
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, normalImage)
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR)
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
    gl.generateMipmap(gl.TEXTURE_2D)

    const u_NormalMap = gl.getUniformLocation(program, 'u_NormalMap')
    gl.uniform1i(u_NormalMap, 1)

    // 开始渲染
    draw()
  }
  normalImage.src = '/texture/raw_plank_wall_nor_gl_2k.jpg'
}

// 设置矩阵
let currentAngle = 0
const setMatrix = () => {
  if (!gl || !program) return

  // 更新旋转角度
  currentAngle = (currentAngle + 0.5) % 360

  // 使用 gl-matrix 创建模型矩阵
  const modelMatrix = mat4.create()
  mat4.rotateY(modelMatrix, modelMatrix, currentAngle * Math.PI / 180)

  // 视图矩阵
  const viewMatrix = mat4.create()
  mat4.translate(viewMatrix, viewMatrix, [0, 0, -3])

  // 投影矩阵
  const projMatrix = mat4.create()
  mat4.perspective(projMatrix, 30 * Math.PI / 180,
    canvas.value!.width / canvas.value!.height, 1, 100)

  // 计算法线矩阵（使用 gl-matrix）
  const normalMatrix = mat3.create()
  const modelView = mat4.create()
  mat4.multiply(modelView, viewMatrix, modelMatrix)
  mat3.normalFromMat4(normalMatrix, modelView)

  // 将矩阵传递给着色器
  const u_ModelMatrix = gl.getUniformLocation(program, 'u_ModelMatrix')
  const u_ViewMatrix = gl.getUniformLocation(program, 'u_ViewMatrix')
  const u_ProjMatrix = gl.getUniformLocation(program, 'u_ProjMatrix')
  const u_NormalMatrix = gl.getUniformLocation(program, 'u_NormalMatrix')

  gl.uniformMatrix4fv(u_ModelMatrix, false, modelMatrix)
  gl.uniformMatrix4fv(u_ViewMatrix, false, viewMatrix)
  gl.uniformMatrix4fv(u_ProjMatrix, false, projMatrix)
  gl.uniformMatrix3fv(u_NormalMatrix, false, normalMatrix)
}

// 绘制函数
const draw = () => {
  if (!gl) return

  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)

  // 更新矩阵
  setMatrix()

  gl.drawElements(gl.TRIANGLES, 36, gl.UNSIGNED_BYTE, 0)
  requestAnimationFrame(draw)
}

// 初始化WebGL
const initWebGL = () => {
  if (!canvas.value) return
  gl = canvas.value.getContext('webgl')
  if (!gl) {
    console.error('Failed to get WebGL context')
    return
  }

  // 创建WebGL工具实例
  const webglUtils = new WebGLUtils(gl)

  // 创建着色器
  const vertexShader = webglUtils.createShader(gl.VERTEX_SHADER, VSHADER_SOURCE)
  const fragmentShader = webglUtils.createShader(gl.FRAGMENT_SHADER, FSHADER_SOURCE)
  if (!vertexShader || !fragmentShader) return

  // 创建程序并链接着色器
  program = webglUtils.createProgram(vertexShader, fragmentShader)
  if (!program) return

  gl.useProgram(program)

  // 设置顶点数据
  initVertexBuffers()

  // 开启深度测试
  gl.enable(gl.DEPTH_TEST)

  // 设置清除颜色和深度缓冲区
  gl.clearColor(0.0, 0.0, 0.0, 1.0)
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)

  // 设置固定光源位置
  setLightPosition()

  const u_NormalScale = gl.getUniformLocation(program, 'u_NormalScale')
  gl.uniform1f(u_NormalScale, normalMapForm.normalScale)

  // 设置纹理
  initTextures()
}

// 组件挂载时初始化WebGL
onMounted(() => {
  initWebGL()
})
</script>

<style lang="scss" scoped>
.normal-map {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;

  h1 {
    font-size: 2em;
    color: #333;
    margin-bottom: 1rem;
  }

  p {
    color: #666;
    line-height: 1.6;
    margin-bottom: 1.5rem;
  }

  .demo-section {
    margin: 20px 0;

    .demo-content {
      display: flex;
      gap: 30px;
      align-items: flex-start;

      canvas {
        border: 2px solid #e6e6e6;
        border-radius: 8px;
        box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
      }

      .control-panel {
        flex: 1;
        padding: 20px;
        background-color: #f8f9fa;
        border-radius: 8px;
        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);

        .light-position-control {
        width: 100%;
          display: flex;
          flex-direction: column;
          gap: 10px;
        }

        .el-form {
          margin-bottom: 20px;
        }

        p {
          font-size: 14px;
          color: #606266;
          margin: 15px 0;
        }

        ul {
          list-style-type: none;
          padding-left: 0;
          margin: 10px 0;

          li {
            margin: 8px 0;
            color: #606266;
            font-size: 14px;
            position: relative;
            padding-left: 20px;

            &::before {
              content: "•";
              position: absolute;
              left: 0;
              color: #409eff;
            }
          }
        }
      }
    }
  }

  .code-section {
    margin: 20px 0;

    .code-content {
      background-color: #f8f9fa;
      padding: 20px;
      border-radius: 8px;
      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);

      pre {
        margin: 0;
        white-space: pre-wrap;
        font-family: "Monaco", "Menlo", "Consolas", monospace;

        code {
          color: #409eff;
        }
      }
    }
  }
}
</style>
